package org.graylog2.log4j2;

import static java.util.Objects.requireNonNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AppenderLoggingException;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.net.Severity;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.status.StatusLogger;
import org.graylog2.gelfclient.GelfConfiguration;
import org.graylog2.gelfclient.GelfMessage;
import org.graylog2.gelfclient.GelfMessageBuilder;
import org.graylog2.gelfclient.GelfMessageLevel;
import org.graylog2.gelfclient.GelfTransports;
import org.graylog2.gelfclient.transport.GelfTransport;
import org.springframework.util.ResourceUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;

import cn.sad.zeroboot.core.util.ConvertUtil;

/**
 * 重写org.graylog2.log4j2.GelfAppender，调用graylog的配置更换在starter模块下的graylog.config文件
 * 
 * @author sad
 *
 */
@Plugin(name = "GELF", category = "Core", elementType = "appender", printObject = true)
public class GelfAppender extends AbstractAppender {

	private static Pattern IPV4_PATTERN = Pattern.compile("[:0-9a-f]*(\\d{1,3}\\.){3}(\\d{1,3})");
	private static Pattern IPV6_PATTERN = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}");

	private static final Logger LOG = StatusLogger.getLogger();

	private final GelfConfiguration gelfConfiguration;
	private final String hostName;
	private final boolean includeSource;
	private final boolean includeThreadContext;
	private final boolean includeStackTrace;
	private final boolean includeExceptionCause;
	private final Map<String, Object> additionalFields;

	private GelfTransport client;

	@SuppressWarnings("deprecation")
	protected GelfAppender(final String name, final Layout<? extends Serializable> layout, final Filter filter, final boolean ignoreExceptions, final GelfConfiguration gelfConfiguration,
			final String hostName, final boolean includeSource, final boolean includeThreadContext, final boolean includeStackTrace, final KeyValuePair[] additionalFields,
			final boolean includeExceptionCause) {
		super(name, filter, layout, ignoreExceptions);

		// 从 graylog.config 文件中读取配置，如果没有这个文件则使用默认的
		String configContent = getGraylogConfigContent();
		if (StringUtils.isNotBlank(configContent)) {
			Map<String, Object> data = JSON.parseObject(configContent, new TypeReference<Map<String, Object>>() {
			});
			GelfConfiguration instance = new GelfConfiguration(ConvertUtil.strOf(data.get("server")), ConvertUtil.intOf(data.get("port")));
			instance.transport(GelfTransports.UDP);
			this.gelfConfiguration = instance;

		} else {
			this.gelfConfiguration = gelfConfiguration;
		}

		this.hostName = hostName;
		this.includeSource = includeSource;
		this.includeThreadContext = includeThreadContext;
		this.includeStackTrace = includeStackTrace;
		this.includeExceptionCause = includeExceptionCause;

		if (null != additionalFields) {
			this.additionalFields = new HashMap<>();
			for (KeyValuePair pair : additionalFields) {
				this.additionalFields.put(pair.getKey(), pair.getValue());
			}
		} else {
			this.additionalFields = Collections.emptyMap();
		}
	}

	@SuppressWarnings("deprecation")
	@Override
	public void append(LogEvent event) {
		final Layout<? extends Serializable> layout = getLayout();
		final String formattedMessage;
		if (layout == null) {
			formattedMessage = event.getMessage().getFormattedMessage();
		} else {
			formattedMessage = new String(layout.toByteArray(event), StandardCharsets.UTF_8);
		}

		final GelfMessageBuilder builder = new GelfMessageBuilder(formattedMessage, hostName).timestamp(event.getTimeMillis() / 1000d)
				.level(GelfMessageLevel.fromNumericLevel(Severity.getSeverity(event.getLevel()).getCode())).additionalField("loggerName", event.getLoggerName())
				.additionalField("threadName", event.getThreadName());

		final Marker marker = event.getMarker();
		if (marker != null) {
			builder.additionalField("marker", marker.getName());
		}

		if (includeThreadContext) {
			for (Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
				builder.additionalField(entry.getKey(), entry.getValue());
			}

			// Guard against https://issues.apache.org/jira/browse/LOG4J2-1530
			final ThreadContext.ContextStack contextStack = event.getContextStack();
			if (contextStack != null) {
				final List<String> contextStackItems = contextStack.asList();
				if (contextStackItems != null && !contextStackItems.isEmpty()) {
					builder.additionalField("contextStack", contextStackItems.toString());
				}
			}
		}

		if (includeSource) {
			final StackTraceElement source = event.getSource();
			if (source != null) {
				builder.additionalField("sourceFileName", source.getFileName());
				builder.additionalField("sourceMethodName", source.getMethodName());
				builder.additionalField("sourceClassName", source.getClassName());
				builder.additionalField("sourceLineNumber", source.getLineNumber());
			}
		}

		@SuppressWarnings("all")
		final Throwable thrown = event.getThrown();
		if (includeStackTrace && thrown != null) {
			String stackTrace;
			if (includeExceptionCause) {
				final StringWriter stringWriter = new StringWriter();
				final PrintWriter printWriter = new PrintWriter(stringWriter);
				thrown.printStackTrace(printWriter);
				stackTrace = stringWriter.toString();
			} else {
				stackTrace = getSimpleStacktraceAsString(thrown);
			}

			builder.additionalField("exceptionClass", thrown.getClass().getCanonicalName());
			builder.additionalField("exceptionMessage", thrown.getMessage());
			builder.additionalField("exceptionStackTrace", stackTrace);

			builder.fullMessage(formattedMessage);
		}

		if (!additionalFields.isEmpty()) {
			builder.additionalFields(additionalFields);
		}

		final GelfMessage gelfMessage = builder.build();
		try {
			final boolean sent = client.trySend(gelfMessage);
			if (!sent) {
				LOG.debug("Couldn't send message: {}", gelfMessage);
			}
		} catch (Exception e) {
			throw new AppenderLoggingException("failed to write log event to GELF server: " + e.getMessage(), e);
		}
	}

	@SuppressWarnings("resource")
	protected String getSimpleStacktraceAsString(final Throwable thrown) {
		final StringBuilder stackTraceBuilder = new StringBuilder();
		for (StackTraceElement stackTraceElement : thrown.getStackTrace()) {
			new Formatter(stackTraceBuilder).format("%s.%s(%s:%d)%n", stackTraceElement.getClassName(), stackTraceElement.getMethodName(), stackTraceElement.getFileName(),
					stackTraceElement.getLineNumber());
		}
		return stackTraceBuilder.toString();
	}

	protected void setClient(GelfTransport client) {
		this.client = requireNonNull(client);
	}

	@Override
	public void start() {
		super.start();
		client = GelfTransports.create(gelfConfiguration);
	}

	@Override
	public void stop() {
		super.stop();
		if (client != null) {
			client.stop();
		}
	}

	@Override
	public String toString() {
		return GelfAppender.class.getSimpleName() + "{" + "name=" + getName() + ",server=" + gelfConfiguration.getRemoteAddress().getHostName() + ",port="
				+ gelfConfiguration.getRemoteAddress().getPort() + ",protocol=" + gelfConfiguration.getTransport().toString() + ",hostName=" + hostName + ",queueSize="
				+ gelfConfiguration.getQueueSize() + ",connectTimeout=" + gelfConfiguration.getConnectTimeout() + ",reconnectDelay=" + gelfConfiguration.getReconnectDelay() + ",sendBufferSize="
				+ gelfConfiguration.getSendBufferSize() + ",tcpNoDelay=" + gelfConfiguration.isTcpNoDelay() + ",tcpKeepAlive=" + gelfConfiguration.isTcpKeepAlive() + ",tlsEnabled="
				+ gelfConfiguration.isTlsEnabled() + ",tlsCertVerificationEnabled=" + gelfConfiguration.isTlsCertVerificationEnabled() + ",tlsTrustCertChainFilename="
				+ (gelfConfiguration.getTlsTrustCertChainFile() != null ? gelfConfiguration.getTlsTrustCertChainFile().getPath() : "null") + "}";
	}

	/**
	 * Factory method for creating a {@link GelfTransport} provider within the plugin manager.
	 *
	 * @param name                             The name of the Appender.
	 * @param filter                           A Filter to determine if the event should be handled by this Appender.
	 * @param layout                           The Layout to use to format the LogEvent defaults to {@code "%m%n"}.
	 * @param ignoreExceptions                 The default is {@code true}, causing exceptions encountered while appending
	 *                                         events to be internally logged and then ignored. When set to {@code false}
	 *                                         exceptions will be propagated to the caller, instead. Must be set to
	 *                                         {@code false} when wrapping this Appender in a
	 *                                         {@link org.apache.logging.log4j.core.appender.FailoverAppender}.
	 * @param server                           The server name of the GELF server, defaults to {@code localhost}.
	 * @param port                             The port the GELF server is listening on, defaults to {@code 12201}.
	 * @param hostName                         The host name of the machine generating the logs, defaults to local host name
	 *                                         or {@code localhost} if it couldn't be detected.
	 * @param protocol                         The transport protocol to use, defaults to {@code UDP}.
	 * @param tlsEnabled                       Whether TLS should be enabled, defaults to {@code false}.
	 * @param tlsEnableCertificateVerification Whether TLS certificate chain should be checked, defaults to {@code true}.
	 * @param tlsTrustCertChainFilename        A X.509 certificate chain file in PEM format for certificate verification,
	 *                                         defaults to {@code null}
	 * @param queueSize                        The size of the internally used queue, defaults to {@code 512}.
	 * @param connectTimeout                   The connection timeout for TCP connections in milliseconds, defaults to
	 *                                         {@code 1000}.
	 * @param reconnectDelay                   The time to wait between reconnects in milliseconds, defaults to {@code 500}.
	 * @param sendBufferSize                   The size of the socket send buffer in bytes, defaults to {@code -1}
	 *                                         (deactivate).
	 * @param tcpNoDelay                       Whether Nagle's algorithm should be used for TCP connections, defaults to
	 *                                         {@code false}.
	 * @param tcpKeepAlive                     Whether to try keeping alive TCP connections, defaults to {@code false}.
	 * @param includeSource                    Whether the source of the log message should be included, defaults to
	 *                                         {@code true}.
	 * @param includeThreadContext             Whether the contents of the {@link org.apache.logging.log4j.ThreadContext}
	 *                                         should be included, defaults to {@code true}.
	 * @param includeStackTrace                Whether a full stack trace should be included, defaults to {@code true}.
	 * @param includeExceptionCause            Whether the included stack trace should contain causing exceptions, defaults
	 *                                         to {@code false}.
	 * @param additionalFields                 Additional static key=value pairs that will be added to every log message.
	 * @return a new GELF provider
	 */
	@PluginFactory
	@SuppressWarnings("unused")
	public static GelfAppender createGelfAppender(@PluginElement("Filter") Filter filter, @PluginElement("Layout") Layout<? extends Serializable> layout,
			@PluginElement(value = "AdditionalFields") final KeyValuePair[] additionalFields, @PluginAttribute(value = "name") String name,
			@PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) Boolean ignoreExceptions, @PluginAttribute(value = "server", defaultString = "localhost") String server,
			@PluginAttribute(value = "port", defaultInt = 12201) Integer port, @PluginAttribute(value = "protocol", defaultString = "UDP") String protocol,
			@PluginAttribute(value = "hostName") String hostName, @PluginAttribute(value = "queueSize", defaultInt = 512) Integer queueSize,
			@PluginAttribute(value = "connectTimeout", defaultInt = 1000) Integer connectTimeout, @PluginAttribute(value = "reconnectDelay", defaultInt = 500) Integer reconnectDelay,
			@PluginAttribute(value = "sendBufferSize", defaultInt = -1) Integer sendBufferSize, @PluginAttribute(value = "tcpNoDelay", defaultBoolean = false) Boolean tcpNoDelay,
			@PluginAttribute(value = "tcpKeepAlive", defaultBoolean = false) Boolean tcpKeepAlive, @PluginAttribute(value = "includeSource", defaultBoolean = true) Boolean includeSource,
			@PluginAttribute(value = "includeThreadContext", defaultBoolean = true) Boolean includeThreadContext,
			@PluginAttribute(value = "includeStackTrace", defaultBoolean = true) Boolean includeStackTrace,
			@PluginAttribute(value = "includeExceptionCause", defaultBoolean = false) Boolean includeExceptionCause, @PluginAttribute(value = "tlsEnabled", defaultBoolean = false) Boolean tlsEnabled,
			@PluginAttribute(value = "tlsEnableCertificateVerification", defaultBoolean = true) Boolean tlsEnableCertificateVerification,
			@PluginAttribute(value = "tlsTrustCertChainFilename") String tlsTrustCertChainFilename) {
		if (name == null) {
			LOGGER.error("No name provided for ConsoleAppender");
			return null;
		}

		if (!"UDP".equalsIgnoreCase(protocol) && !"TCP".equalsIgnoreCase(protocol)) {
			LOG.warn("Invalid protocol {}, falling back to UDP", protocol);
			protocol = "UDP";
		}
		if (hostName == null || hostName.trim().isEmpty()) {
			try {
				final String canonicalHostName = InetAddress.getLocalHost().getCanonicalHostName();
				if (isFQDN(canonicalHostName)) {
					hostName = canonicalHostName;
				} else {
					hostName = InetAddress.getLocalHost().getHostName();
				}
			} catch (UnknownHostException e) {
				LOG.warn("Couldn't detect local host name, falling back to \"localhost\"");
				hostName = "localhost";
			}
		}

		final InetSocketAddress serverAddress = new InetSocketAddress(server, port);
		final GelfTransports gelfProtocol = GelfTransports.valueOf(protocol.toUpperCase());
		final GelfConfiguration gelfConfiguration = new GelfConfiguration(serverAddress).transport(gelfProtocol).queueSize(queueSize).connectTimeout(connectTimeout).reconnectDelay(reconnectDelay)
				.sendBufferSize(sendBufferSize).tcpNoDelay(tcpNoDelay).tcpKeepAlive(tcpKeepAlive);

		if (tlsEnabled) {
			if (gelfProtocol.equals(GelfTransports.TCP)) {
				gelfConfiguration.enableTls();
				if (!tlsEnableCertificateVerification) {
					LOG.warn("TLS certificate validation is disabled. This is unsecure!");
					gelfConfiguration.disableTlsCertVerification();
				}
				if (tlsEnableCertificateVerification && tlsTrustCertChainFilename != null) {
					gelfConfiguration.tlsTrustCertChainFile(new File(tlsTrustCertChainFilename));
				}
			} else {
				LOG.warn("Enabling of TLS is invalid for UDP Transport");
			}
		}

		return new GelfAppender(name, layout, filter, ignoreExceptions, gelfConfiguration, hostName, includeSource, includeThreadContext, includeStackTrace, additionalFields, includeExceptionCause);
	}

	static boolean isFQDN(String canonicalHostName) {
		return canonicalHostName.contains(".") && !IPV4_PATTERN.matcher(canonicalHostName).matches() && !IPV6_PATTERN.matcher(canonicalHostName).matches();
	}

	static String getGraylogConfigContent() {
		File file = null;
		BufferedReader reader = null;
		try {
			file = ResourceUtils.getFile("classpath:graylog.config");
			if (file != null && file.exists()) {
				reader = new BufferedReader(new FileReader(file));
				StringBuffer buffer = new StringBuffer(1024 * 10);
				String temp = null;
				while ((temp = reader.readLine()) != null) {
					buffer.append(temp);
				}
				String content = buffer.toString();

				return content;
			}
		} catch (FileNotFoundException e) {
			System.out.println("Not find graylog.config file!");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}
}
